/*******************************************************************************
 * This file is part of Crunch Graphics.
 *
 * Crunch Graphics is free software: you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later version.
 *
 * Crunch Graphics is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along with
 * Crunch Graphics.  If not, see <http://www.gnu.org/licenses/>.
 ******************************************************************************/

package com.crunch.graphics.gpu;

import javax.media.opengl.GL;
import javax.media.opengl.GL3;

/** Encapsulates a framebuffer on the GPU.
 */
public class GpuFramebuffer extends ReferenceCountedGpuObject {
	/** Returns the ID of the underlying buffer.
	 */
	public int getId() {
		return id;
	}

	/** Binds the buffer to the given location.
	 */
	public void bind(int location) {
		getGpuState().bindFramebuffer(location, this);
	}

	/** Attaches the renderbuffer to the given attachment location.
	 */
	public void attach(int attachment, GpuRenderbuffer renderbuffer) {
		if (attachment == GL3.GL_DEPTH_STENCIL_ATTACHMENT) {
			if (depthAttachment.renderbuffer == renderbuffer &&
					stencilAttachment.renderbuffer == renderbuffer)
				// already attached
				return;

			bind(GL.GL_FRAMEBUFFER);
			getGpuState().getGl().glFramebufferRenderbuffer(GL.GL_FRAMEBUFFER, attachment, GL.GL_RENDERBUFFER, renderbuffer.getId());

			if (renderbuffer != null) {
				renderbuffer.acquire();
			}
			if (depthAttachment.renderbuffer != null) {
				depthAttachment.renderbuffer.release();
			}
			if (depthAttachment.texture != null) {
				depthAttachment.texture.release();
			}
			depthAttachment.renderbuffer = renderbuffer;
			depthAttachment.texture = null;
			depthAttachment.mipmapLevel = 0;
			depthAttachment.layer = 0;

			if (renderbuffer != null) {
				renderbuffer.acquire();
			}
			if (stencilAttachment.renderbuffer != null) {
				stencilAttachment.renderbuffer.release();
			}
			if (stencilAttachment.texture != null) {
				stencilAttachment.texture.release();
			}
			stencilAttachment.renderbuffer = renderbuffer;
			stencilAttachment.texture = null;
			stencilAttachment.mipmapLevel = 0;
			stencilAttachment.layer = 0;
		} else {
			Attachment att = getAttachment(attachment);
			if (att.renderbuffer == renderbuffer) {
				// already attached
				return;
			}

			bind(GL.GL_FRAMEBUFFER);
			getGpuState().getGl().glFramebufferRenderbuffer(GL.GL_FRAMEBUFFER, attachment, GL.GL_RENDERBUFFER, renderbuffer.getId());

			if (renderbuffer != null) {
				renderbuffer.acquire();
			}
			if (att.renderbuffer != null) {
				att.renderbuffer.release();
			}
			if (att.texture != null) {
				att.texture.release();
			}
			att.renderbuffer = renderbuffer;
			att.texture = null;
			att.mipmapLevel = 0;
			att.layer = 0;
		}
	}

	/** Attaches the texture to the given attachment location.
	 */
	public void attach(int attachment, GpuTexture texture) {
		attach(attachment, texture, 0);
	}

	/** Attaches the texture to the given attachment location.
	 */
	public void attach(int attachment, GpuTexture texture, int mipmapLevel) {
		if (attachment == GL3.GL_DEPTH_STENCIL_ATTACHMENT) {
			if (depthAttachment.texture == texture && depthAttachment.mipmapLevel == mipmapLevel && !depthAttachment.layered &&
					stencilAttachment.texture == texture && stencilAttachment.mipmapLevel == mipmapLevel && !stencilAttachment.layered)
				// already attached
				return;

			bind(GL.GL_FRAMEBUFFER);
			getGpuState().getGl().glFramebufferTexture(GL.GL_FRAMEBUFFER, attachment, texture.getId(), mipmapLevel);

			if (texture != null) {
				texture.acquire();
			}
			if (depthAttachment.renderbuffer != null) {
				depthAttachment.renderbuffer.release();
			}
			if (depthAttachment.texture != null) {
				depthAttachment.texture.release();
			}
			depthAttachment.renderbuffer = null;
			depthAttachment.texture = texture;
			depthAttachment.mipmapLevel = mipmapLevel;
			depthAttachment.layer = 0;
			depthAttachment.layered = false;

			if (texture != null) {
				texture.acquire();
			}
			if (stencilAttachment.renderbuffer != null) {
				stencilAttachment.renderbuffer.release();
			}
			if (stencilAttachment.texture != null) {
				stencilAttachment.texture.release();
			}
			stencilAttachment.renderbuffer = null;
			stencilAttachment.texture = texture;
			stencilAttachment.mipmapLevel = mipmapLevel;
			stencilAttachment.layer = 0;
			stencilAttachment.layered = false;
		} else {
			Attachment att = getAttachment(attachment);
			if (att.texture == texture && att.mipmapLevel == mipmapLevel && !att.layered) {
				// already attached
				return;
			}

			bind(GL.GL_FRAMEBUFFER);
			getGpuState().getGl().glFramebufferTexture(GL.GL_FRAMEBUFFER, attachment, texture.getId(), mipmapLevel);

			if (texture != null) {
				texture.acquire();
			}
			if (att.renderbuffer != null) {
				att.renderbuffer.release();
			}
			if (att.texture != null) {
				att.texture.release();
			}
			att.renderbuffer = null;
			att.texture = texture;
			att.mipmapLevel = mipmapLevel;
			att.layer = 0;
			att.layered = false;
		}
	}

	/** Attaches the layer in the texture to the given attachment location.
	 */
	public void attachLayer(int attachment, GpuTexture texture, int layer) {
		attachLayer(attachment, texture, layer, 0);
	}

	/** Attaches the layer in the texture to the given attachment location.
	 */
	public void attachLayer(int attachment, GpuTexture texture, int layer, int mipmapLevel) {
		if (attachment == GL3.GL_DEPTH_STENCIL_ATTACHMENT) {
			if (depthAttachment.texture == texture && depthAttachment.mipmapLevel == mipmapLevel && depthAttachment.layered && depthAttachment.layer == layer &&
					stencilAttachment.texture == texture && stencilAttachment.mipmapLevel == mipmapLevel && stencilAttachment.layered && stencilAttachment.layer == layer)
				// already attached
				return;

			bind(GL.GL_FRAMEBUFFER);
			// glFramebufferTextureLayer() doesn't seem to work for cube maps
			if (texture.getType() == GL.GL_TEXTURE_CUBE_MAP) {
				getGpuState().getGl().glFramebufferTexture2D(GL.GL_FRAMEBUFFER, attachment, GL.GL_TEXTURE_CUBE_MAP_POSITIVE_X + layer, texture.getId(), mipmapLevel);
			} else {
				getGpuState().getGl().glFramebufferTextureLayer(GL.GL_FRAMEBUFFER, attachment, texture.getId(), mipmapLevel, layer);
			}

			if (texture != null) {
				texture.acquire();
			}
			if (depthAttachment.renderbuffer != null) {
				depthAttachment.renderbuffer.release();
			}
			if (depthAttachment.texture != null) {
				depthAttachment.texture.release();
			}
			depthAttachment.renderbuffer = null;
			depthAttachment.texture = texture;
			depthAttachment.mipmapLevel = mipmapLevel;
			depthAttachment.layer = layer;
			depthAttachment.layered = true;

			if (texture != null) {
				texture.acquire();
			}
			if (stencilAttachment.renderbuffer != null) {
				stencilAttachment.renderbuffer.release();
			}
			if (stencilAttachment.texture != null) {
				stencilAttachment.texture.release();
			}
			stencilAttachment.renderbuffer = null;
			stencilAttachment.texture = texture;
			stencilAttachment.mipmapLevel = mipmapLevel;
			stencilAttachment.layer = layer;
			stencilAttachment.layered = true;
		} else {
			Attachment att = getAttachment(attachment);
			if (att.texture == texture && att.mipmapLevel == mipmapLevel && att.layered && att.layer == layer) {
				// already attached
				return;
			}

			bind(GL.GL_FRAMEBUFFER);
			// glFramebufferTextureLayer() doesn't seem to work for cube maps
			if (texture.getType() == GL.GL_TEXTURE_CUBE_MAP) {
				getGpuState().getGl().glFramebufferTexture2D(GL.GL_FRAMEBUFFER, attachment, GL.GL_TEXTURE_CUBE_MAP_POSITIVE_X + layer, texture.getId(), mipmapLevel);
			} else {
				getGpuState().getGl().glFramebufferTextureLayer(GL.GL_FRAMEBUFFER, attachment, texture.getId(), mipmapLevel, layer);
			}

			if (texture != null) {
				texture.acquire();
			}
			if (att.renderbuffer != null) {
				att.renderbuffer.release();
			}
			if (att.texture != null) {
				att.texture.release();
			}
			att.renderbuffer = null;
			att.texture = texture;
			att.mipmapLevel = mipmapLevel;
			att.layer = layer;
			att.layered = true;
		}
	}

	/** Detatches the texture or renderbuffer from the given attachment location.
	 */
	public void detatch(int attachment) {
		if (attachment == GL3.GL_DEPTH_STENCIL_ATTACHMENT) {
			if (depthAttachment.renderbuffer == null && depthAttachment.texture == null &&
					stencilAttachment.renderbuffer == null && stencilAttachment.texture == null)
				// already detatched
				return;

			bind(GL.GL_FRAMEBUFFER);
			getGpuState().getGl().glFramebufferRenderbuffer(GL.GL_FRAMEBUFFER, attachment, GL.GL_RENDERBUFFER, 0);

			if (depthAttachment.renderbuffer != null) {
				depthAttachment.renderbuffer.release();
			}
			if (depthAttachment.texture != null) {
				depthAttachment.texture.release();
			}
			depthAttachment.renderbuffer = null;
			depthAttachment.texture = null;
			depthAttachment.mipmapLevel = 0;
			depthAttachment.layer = 0;

			if (stencilAttachment.renderbuffer != null) {
				stencilAttachment.renderbuffer.release();
			}
			if (stencilAttachment.texture != null) {
				stencilAttachment.texture.release();
			}
			stencilAttachment.renderbuffer = null;
			stencilAttachment.texture = null;
			stencilAttachment.mipmapLevel = 0;
			stencilAttachment.layer = 0;
		} else {
			Attachment att = getAttachment(attachment);
			if (att.renderbuffer == null && att.texture == null) {
				// already detached
				return;
			}

			bind(GL.GL_FRAMEBUFFER);
			getGpuState().getGl().glFramebufferRenderbuffer(GL.GL_FRAMEBUFFER, attachment, GL.GL_RENDERBUFFER, 0);

			if (att.renderbuffer != null) {
				att.renderbuffer.release();
			}
			if (att.texture != null) {
				att.texture.release();
			}
			att.renderbuffer = null;
			att.texture = null;
			att.mipmapLevel = 0;
			att.layer = 0;
		}
	}

	/** Returns the renderbuffer attached to the given attachment location, or an empty reference if none is attached.
	 */
	public GpuRenderbuffer getAttachedRenderbuffer(int attachment) {
		return getAttachment(attachment).renderbuffer;
	}

	/** Returns the texture attached to the given attachment location, or an empty reference if none is attached.
	 */
	public GpuTexture getAttachedTexture(int attachment) {
		return getAttachment(attachment).texture;
	}

	/** Returns the mipmap level of the texture attached to the given attachment location.
	 */
	public int getAttachmentLevel(int attachment) {
		return getAttachment(attachment).mipmapLevel;
	}

	/** Returns the layer of the texture attached to the given attachment location.
	 */
	public int getAttachmentLayer(int attachment) {
		return getAttachment(attachment).layer;
	}

	GpuFramebuffer(GpuState state) {
		super(state);
		int[] buf = new int[1];
		getGpuState().getGl().glGenFramebuffers(1, buf, 0);
		id = buf[0];
		bind(GL.GL_FRAMEBUFFER);
		colorAttachments = new Attachment[getGpuState().getMaxColorAttachments()];
		for (int i = 0; i < colorAttachments.length; ++i) {
			colorAttachments[i] = new Attachment();
		}
		depthAttachment = new Attachment();
		stencilAttachment = new Attachment();
		depthAttachment.mipmapLevel = 0;
		depthAttachment.layer = 0;
		depthAttachment.layered = false;
		stencilAttachment.mipmapLevel = 0;
		stencilAttachment.layer = 0;
		stencilAttachment.layered = false;
		for (int i = 0; i < colorAttachments.length; ++i) {
			colorAttachments[i].mipmapLevel = 0;
			colorAttachments[i].layer = 0;
			colorAttachments[i].layered = false;
		}
	}

	protected void onRelease() {
		getGpuState().getGl().glDeleteFramebuffers(1, new int[] { id }, 0);
	}

	private int id;

	// attachment holds either renderbuffer OR texture (or none)
	private static class Attachment {
		public GpuRenderbuffer renderbuffer;
		public GpuTexture texture;
		public int mipmapLevel;
		public int layer;
		public boolean layered;
	};
	private Attachment getAttachment(int location) {
		if (location == GL.GL_DEPTH_ATTACHMENT) {
			return depthAttachment;
		} else if (location == GL.GL_STENCIL_ATTACHMENT) {
			return stencilAttachment;
		} else {
			if (location < GL.GL_COLOR_ATTACHMENT0 || location >= GL.GL_COLOR_ATTACHMENT0 + colorAttachments.length) {
				throw new IllegalArgumentException( "Invalid framebuffer attachment" );
			} else {
				return colorAttachments[location - GL.GL_COLOR_ATTACHMENT0];
			}
		}
	}

	private Attachment depthAttachment;
	private Attachment stencilAttachment;
	private Attachment[] colorAttachments;
}